// Created by Roman Novitsky on 30.04.2009


#region Imported Namespaces


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Shapes;

using WtfManager.Framework;
using WtfManager.Utilities;
using WtfManager.WindowsNative.User32;


#endregion


namespace WtfManager.UIManager
{
  public class DragDropManager: ExtensibleService
  {
    #region Fields


    public const string WtfManagerItemsListDataFormat = "WtfManagerItemsList";


    private static readonly List<UIElement> _pendingDragableUIElements = new List<UIElement>();
    private static readonly List<UIElement> _pendingDropTargets = new List<UIElement>();

    public static readonly DependencyProperty AllowDragProperty = DependencyProperty.RegisterAttached("AllowDrag",
                                                                                                      typeof(bool),
                                                                                                      typeof(
                                                                                                        DragDropManager));

    public static readonly DependencyProperty AllowDropProperty = DependencyProperty.RegisterAttached("AllowDrop",
                                                                                                      typeof(bool),
                                                                                                      typeof(
                                                                                                        DragDropManager),
                                                                                                      new PropertyMetadata
                                                                                                        (AllowDropPropertyChanged));

    public static readonly DependencyProperty IsDragableProperty = DependencyProperty.RegisterAttached("IsDragable",
                                                                                                       typeof(bool),
                                                                                                       typeof(
                                                                                                         DragDropManager
                                                                                                         ),
                                                                                                       new PropertyMetadata
                                                                                                         (IsDragablePropertyChanged));

    private readonly List<IDragDataFormatExtension> _dataFormatExtensions = new List<IDragDataFormatExtension>();


    private readonly TypeKeyDictionary<IDragDropHandlerExtension> _dropHandlers =
      new TypeKeyDictionary<IDragDropHandlerExtension>();

    private ConverterService _converterService;

    private Window _dragDropVisualWindow;
    private Point _dragDropVisualWindowOffset;
    private DragStartInfo _dragStartInfo;


    #endregion


    #region Constructors/Finalizers


    public DragDropManager()
    {
      DragDelta = 5;
      DragDropVisualOpacity = 0.5;
    }


    #endregion


    #region Properties/Indexers


    public override string Description
    {
      get
      {
        return "Manages Drag and Drop operations.";
      }
    }


    public double DragDelta
    {
      get;
      set;
    }


    public double DragDropVisualOpacity
    {
      get;
      set;
    }

    private bool IsDragging
    {
      get;
      set;
    }


    public override string Name
    {
      get
      {
        return "Drag Drop Manager";
      }
    }


    #endregion


    #region Methods


    private static void IsDragablePropertyChanged(DependencyObject dependencyObject,
                                                  DependencyPropertyChangedEventArgs e)
    {
      var element = dependencyObject as UIElement;

      if (element == null)
      {
        return;
      }

      var value = (bool)e.NewValue;
      var service = (DragDropManager)WtfFramework.Instance.Services[typeof(DragDropManager)];

      if (service == null || !service.IsInitialized)
      {
        if (value)
        {
          _pendingDragableUIElements.Add(element);
        }

        return;
      }

      if (value)
      {
        service.EnableDrag(element);
      }
      else
      {
        service.DisableDrag(element);
      }
    }


    private static void AllowDropPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
      var element = dependencyObject as UIElement;

      if (element == null)
      {
        return;
      }

      var value = (bool)e.NewValue;
      var service = (DragDropManager)WtfFramework.Instance.Services[typeof(DragDropManager)];

      if (service == null || !service.IsInitialized)
      {
        if (value)
        {
          _pendingDropTargets.Add(element);
        }
        return;
      }

      if (value)
      {
        service.EnableDrop(element);
      }
      else
      {
        service.DisableDrop(element);
      }
    }


    private void DisableDrop(UIElement dropTarget)
    {
      dropTarget.AllowDrop = false;
      dropTarget.Drop -= DropTargetDrop;
      dropTarget.DragEnter -= DropTargetDragOver;
      dropTarget.DragOver -= DropTargetDragOver;
    }


    private void EnableDrop(UIElement dropTarget)
    {
      dropTarget.AllowDrop = true;
      dropTarget.Drop += DropTargetDrop;
      dropTarget.DragEnter += DropTargetDragOver;
      dropTarget.DragOver += DropTargetDragOver;
    }


    private void DropTargetDragOver(object sender, DragEventArgs e)
    {
      e.Handled = true;
      e.Effects = DragDropEffects.None;

      List<object> draggedItems = GetDraggedItems(e);
      object dropTarget = GetDropTarget(sender);

      if (dropTarget == null)
      {
        return;
      }

      IDragDropHandlerExtension extension = GetDropHandler(dropTarget);

      if (extension == null)
      {
        return;
      }

      List<object> droppedItems = GetDroppedItems(extension, draggedItems);

      if (droppedItems.Count > 0)
      {
        e.Effects = extension.GetDropEffect(dropTarget, droppedItems, e.KeyStates);
      }
    }


    private void DropTargetDrop(object sender, DragEventArgs e)
    {
      List<object> draggedItems = GetDraggedItems(e);

      if (draggedItems == null)
      {
        return;
      }

      DoDrop(draggedItems, e.Source, e.Effects);

      e.Handled = true;
    }


    private static List<object> GetDraggedItems(DragEventArgs e)
    {
      // TODO <RNovitsky, 19.05.2009>: Add extension support.
      return e.Data.GetData(WtfManagerItemsListDataFormat) as List<object>;
    }


    private void DoDrop(List<object> draggedItems, object dropTargetControl, DragDropEffects effects)
    {
      object dropTarget = GetDropTarget(dropTargetControl);

      if (dropTarget == null)
      {
        return;
      }

      IDragDropHandlerExtension extension = GetDropHandler(dropTarget);

      if (extension == null)
      {
        return;
      }

      List<object> droppedItems = GetDroppedItems(extension, draggedItems);

      if (droppedItems.Count > 0)
      {
        extension.DoDrop(dropTarget, droppedItems, effects);
      }
    }


    private List<object> GetDroppedItems(IDragDropHandlerExtension extension, List<object> draggedItems)
    {
      Type targetType = extension.DroppedItemType;
      var droppedItems = new List<object>();

      foreach (object item in draggedItems)
      {
        Type type = item.GetType();

        object droppedItem = item;

        if (NeedsConversion(targetType, type))
        {
          droppedItem = _converterService.Convert(item, targetType);
        }

        if (droppedItem != null)
        {
          droppedItems.Add(droppedItem);
        }
      }
      return droppedItems;
    }


    private static object GetDropTarget(object dropTargetControl)
    {
      object dropTarget;
      if (dropTargetControl is ContentControl)
      {
        var contentControl = (ContentControl)dropTargetControl;
        dropTarget = contentControl.Content;
      }
      else
      {
        var frameworkElement = (FrameworkElement)dropTargetControl;
        dropTarget = frameworkElement.DataContext;
      }
      return dropTarget;
    }


    private IDragDropHandlerExtension GetDropHandler(object dropTarget)
    {
      return _dropHandlers.GetBestValue(dropTarget.GetType());
    }


    private static bool NeedsConversion(Type targetType, Type type)
    {
      return !targetType.IsAssignableFrom(type);
    }


    private void DisableDrag(IInputElement dragInitiator)
    {
      MouseButtonEventHandler mouseButtonEventHandler = DragInitiatorMouseDown;
      dragInitiator.RemoveHandler(Mouse.PreviewMouseDownEvent, mouseButtonEventHandler);

      MouseButtonEventHandler mouseButtonUpEventHandler = DragInitiatorMouseUp;
      dragInitiator.RemoveHandler(Mouse.MouseUpEvent, mouseButtonUpEventHandler);

      dragInitiator.MouseMove -= DragInitiatorMouseMove;
    }


    private void EnableDrag(UIElement dragInitiator)
    {
      MouseButtonEventHandler mouseButtonDownEventHandler = DragInitiatorMouseDown;
      dragInitiator.AddHandler(Mouse.PreviewMouseDownEvent, mouseButtonDownEventHandler, true);

      MouseButtonEventHandler mouseButtonUpEventHandler = DragInitiatorMouseUp;
      dragInitiator.AddHandler(Mouse.MouseUpEvent, mouseButtonUpEventHandler, true);
    }


    private void DragInitiatorMouseDown(object sender, MouseButtonEventArgs e)
    {
      var dragInitiator = (UIElement)sender;

      if (e.LeftButton != MouseButtonState.Pressed)
      {
        return;
      }

      if (dragInitiator.IsMouseCaptured)
      {
        return;
      }

      if (IsDragging)
      {
        return;
      }

      _dragStartInfo = new DragStartInfo();
      _dragStartInfo.StartPosition = e.GetPosition(dragInitiator);
      _dragStartInfo.DragInitiator = dragInitiator;

      dragInitiator.MouseMove += DragInitiatorMouseMove;
    }


    private static List<object> GetDraggedItems(UIElement dragableItemsContainer)
    {
      if (dragableItemsContainer is ListBox)
      {
        var draggedItems = new List<object>();
        var listView = (ListBox)dragableItemsContainer;

        foreach (object selectedItem in listView.SelectedItems)
        {
          draggedItems.Add(selectedItem);
        }

        return draggedItems;
      }

      if (dragableItemsContainer is TreeView)
      {
        var selector = (TreeView)dragableItemsContainer;

        return new List<object> {selector.SelectedValue};
      }

      if (dragableItemsContainer is Selector)
      {
        var selector = (Selector)dragableItemsContainer;

        return new List<object> {selector.SelectedValue};
      }

      return new List<object>();
    }


    private static UIElement GetDragableItemsContainer(UIElement dragInitiator)
    {
      UIElement dragableItemsContainer = dragInitiator;

      while (dragableItemsContainer != null)
      {
        if (GetAllowDrag(dragableItemsContainer))
        {
          return dragableItemsContainer;
        }

        dragableItemsContainer = (UIElement)VisualTreeHelper.GetParent(dragableItemsContainer);
      }

      return null;
    }


    private void DragInitiatorMouseUp(object sender, MouseButtonEventArgs e)
    {
      var element = (UIElement)sender;

      if (_dragStartInfo != null)
      {
        _dragStartInfo = null;
      }

      element.MouseMove -= DragInitiatorMouseMove;
    }


    private void DragInitiatorMouseMove(object sender, MouseEventArgs e)
    {
      var dragInitiator = (UIElement)sender;

      if (dragInitiator.IsMouseCaptured || IsDragging || e.LeftButton != MouseButtonState.Pressed
          || _dragStartInfo == null || _dragStartInfo.DragInitiator != dragInitiator)
      {
        dragInitiator.MouseMove -= DragInitiatorMouseMove;
        return;
      }

      Point currentPosition = e.GetPosition(dragInitiator);

      if ((Math.Abs(currentPosition.X - _dragStartInfo.StartPosition.X) > DragDelta)
          || (Math.Abs(currentPosition.Y - _dragStartInfo.StartPosition.Y) > DragDelta))
      {
        dragInitiator.MouseMove -= DragInitiatorMouseMove;

        UIElement dragableItemsContainer = GetDragableItemsContainer(dragInitiator);

        if (dragableItemsContainer == null)
        {
          return;
        }

        List<object> draggedItems = GetDraggedItems(dragableItemsContainer);

        if (draggedItems.Count == 0)
        {
          return;
        }

        _dragStartInfo.StartPosition = currentPosition;
        _dragStartInfo.DraggedItems = draggedItems;
        _dragStartInfo.Container = dragableItemsContainer;

        StartDrag(_dragStartInfo);

        _dragStartInfo = null;
      }
      else
      {
        return;
      }
    }


    private void StartDrag(DragStartInfo dragStartInfo)
    {
      UIElement dragInitiator = dragStartInfo.DragInitiator;

      dragInitiator.MouseMove -= DragInitiatorMouseMove;

      SetDragDataFormats(dragStartInfo);

      dragInitiator.CaptureMouse();

      DragDropEffects effect = DragDropEffects.Move;

      if (Keyboard.Modifiers == ModifierKeys.Control)
      {
        effect = DragDropEffects.Copy;
      }

      DoDragDrop(effect, dragStartInfo);

      dragInitiator.ReleaseMouseCapture();
    }


    private void DoDragDrop(DragDropEffects effect, DragStartInfo dragStartInfo)
    {
      UIElement dragInitiator = dragStartInfo.DragInitiator;

      dragInitiator.QueryContinueDrag += DragSourceQueryContinueDrag;

      FrameworkElement visual = GetDragDropVisual(dragInitiator, dragStartInfo);

      if (visual != null)
      {
        CreateDragDropVisualWindow(visual);
      }

      IsDragging = true;

      DragDrop.DoDragDrop(dragInitiator, dragStartInfo.DragDataObject, effect);

      IsDragging = false;

      if (visual != null)
      {
        DestroyDragDropVisualWindow();
      }

      dragInitiator.QueryContinueDrag -= DragSourceQueryContinueDrag;
    }


    private void SetDragDataFormats(DragStartInfo dragStartInfo)
    {
      var dataObject = new DataObject();

      foreach (IDragDataFormatExtension extension in _dataFormatExtensions)
      {
        extension.GetDragFormat(dataObject, dragStartInfo.DraggedItems);
      }

      dataObject.SetData(WtfManagerItemsListDataFormat, dragStartInfo.DraggedItems);

      dragStartInfo.DragDataObject = dataObject;
    }


    private FrameworkElement GetDragDropVisual(Visual dragSource, DragStartInfo dragStartInfo)
    {
      List<UIElement> containers = dragStartInfo.GetItemContainers();

      if (containers.Count == 0)
      {
        return null;
      }

      _dragDropVisualWindowOffset = dragSource.PointToScreen(new Point());

      var containerPositions = new List<Point>();

      foreach (UIElement container in containers)
      {
        Point point = container.PointToScreen(new Point());
        containerPositions.Add(point);
      }

      double xOffset = double.MaxValue;
      double yOffset = double.MaxValue;

      foreach (Point containerPosition in containerPositions)
      {
        if (xOffset > containerPosition.X)
        {
          xOffset = containerPosition.X;
        }

        if (yOffset > containerPosition.Y)
        {
          yOffset = containerPosition.Y;
        }
      }

      for (int i = 0; i < containerPositions.Count; i++)
      {
        Point containerPosition = containerPositions[i];
        containerPosition.X -= xOffset;
        containerPosition.Y -= yOffset;
        containerPositions[i] = containerPosition;
      }

      _dragDropVisualWindowOffset.X -= xOffset;
      _dragDropVisualWindowOffset.Y -= yOffset;

      _dragDropVisualWindowOffset.X += dragStartInfo.StartPosition.X;
      _dragDropVisualWindowOffset.Y += dragStartInfo.StartPosition.Y;

      var canvas = new Canvas();
      double canvasWidth = 0;
      double canvasHeight = 0;

      for (int i = 0; i < containers.Count; i++)
      {
        UIElement container = containers[i];
        Point containerPosition = containerPositions[i];

        Rectangle rectangle = GetDragDropVisual(container);
        canvas.Children.Add(rectangle);

        Canvas.SetLeft(rectangle, containerPosition.X);
        Canvas.SetTop(rectangle, containerPosition.Y);

        double width = containerPosition.X + container.DesiredSize.Width;

        if (canvasWidth < width)
        {
          canvasWidth = width;
        }

        double height = containerPosition.Y + container.DesiredSize.Height;

        if (canvasHeight < height)
        {
          canvasHeight = height;
        }
      }

      canvas.Width = canvasWidth;
      canvas.Height = canvasHeight;

      return canvas;
    }


    private Rectangle GetDragDropVisual(UIElement dragSource)
    {
      var rectangle = new Rectangle();

      rectangle.Width = dragSource.DesiredSize.Width;
      rectangle.Height = dragSource.DesiredSize.Height;

      var visualBrush = new VisualBrush(dragSource);
      visualBrush.Opacity = DragDropVisualOpacity;

      rectangle.Fill = visualBrush;
      return rectangle;
    }


    private void DragSourceQueryContinueDrag(object sender, QueryContinueDragEventArgs e)
    {
      UpdateDargDropVisualWindowLocation();
    }


    private void CreateDragDropVisualWindow(FrameworkElement visual)
    {
      Debug.Assert(_dragDropVisualWindow == null);
      Debug.Assert(visual != null);

      _dragDropVisualWindow = new Window();
      _dragDropVisualWindow.WindowStyle = WindowStyle.None;
      _dragDropVisualWindow.AllowsTransparency = true;
      _dragDropVisualWindow.AllowDrop = false;
      _dragDropVisualWindow.Background = null;
      _dragDropVisualWindow.IsHitTestVisible = false;
      _dragDropVisualWindow.SizeToContent = SizeToContent.WidthAndHeight;
      _dragDropVisualWindow.Topmost = true;
      _dragDropVisualWindow.ShowInTaskbar = false;
      _dragDropVisualWindow.SourceInitialized += OnDragDropVisualWindowSourceInitialized;
      _dragDropVisualWindow.Content = visual;

      // put the window in the right place to start
      UpdateDargDropVisualWindowLocation();

      _dragDropVisualWindow.Show();
    }


    private void OnDragDropVisualWindowSourceInitialized(object sender, EventArgs args)
    {
      PresentationSource windowSource = PresentationSource.FromVisual(_dragDropVisualWindow);

      Debug.Assert(windowSource != null);

      IntPtr handle = ((HwndSource)windowSource).Handle;

      Int32 styles = User32Methods.GetWindowLong(handle, User32Methods.GWL_EXSTYLE);

      User32Methods.SetWindowLong(handle, User32Methods.GWL_EXSTYLE,
                                  styles | User32Methods.WS_EX_LAYERED | User32Methods.WS_EX_TRANSPARENT);
    }


    private void DestroyDragDropVisualWindow()
    {
      if (_dragDropVisualWindow != null)
      {
        _dragDropVisualWindow.Close();
        _dragDropVisualWindow = null;
      }
    }


    private void UpdateDargDropVisualWindowLocation()
    {
      if (_dragDropVisualWindow != null)
      {
        POINT point;

        if (!User32Methods.GetCursorPos(out point))
        {
          return;
        }

        _dragDropVisualWindow.Left = point.X - _dragDropVisualWindowOffset.X;
        _dragDropVisualWindow.Top = point.Y - _dragDropVisualWindowOffset.Y;
      }
    }


    public static void SetAllowDrag(DependencyObject element, bool value)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      element.SetValue(AllowDragProperty, value);
    }


    public static bool GetAllowDrag(DependencyObject element)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      return (bool)element.GetValue(AllowDragProperty);
    }


    public static void SetAllowDrop(DependencyObject element, bool value)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      element.SetValue(AllowDropProperty, value);
    }


    public static bool GetAllowDrop(DependencyObject element)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      return (bool)element.GetValue(AllowDropProperty);
    }


    public static void SetIsDragable(DependencyObject element, bool value)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      element.SetValue(IsDragableProperty, value);
    }


    public static bool GetIsDragable(DependencyObject element)
    {
      if (element == null)
      {
        throw new ArgumentNullException("element");
      }

      return (bool)element.GetValue(IsDragableProperty);
    }


    protected override void OnStateChnaged()
    {
      if (IsInitialized)
      {
        _converterService = (ConverterService)WtfFramework.Instance.Services[typeof(ConverterService)];
      }
      else
      {
        _converterService = null;
      }
    }


    protected override void OnUnload()
    {
    }


    protected override FrameworkComponentState OnInitialize()
    {
      return FrameworkComponentState.Initialized;
    }


    protected override FrameworkComponentState OnLoad()
    {
      AddDependency(typeof(ConverterService));

      return FrameworkComponentState.Loaded;
    }


    protected override void OnExtensionAdded(Service extension)
    {
      if (extension is IDragDataFormatExtension)
      {
        _dataFormatExtensions.Add((IDragDataFormatExtension)extension);
      }

      if (extension is IDragDropHandlerExtension)
      {
        var dropHandlerExtension = (IDragDropHandlerExtension)extension;
        _dropHandlers.Add(dropHandlerExtension.DropTargetType, dropHandlerExtension);
      }
    }


    public override bool IsExtension(Service extension)
    {
      return extension is IDragDataFormatExtension || extension is IDragDropHandlerExtension;
    }


    protected override void OnExtensionRemoved(Service extension)
    {
      if (extension is IDragDataFormatExtension)
      {
        _dataFormatExtensions.Remove((IDragDataFormatExtension)extension);
      }

      if (extension is IDragDropHandlerExtension)
      {
        var dropHandlerExtension = (IDragDropHandlerExtension)extension;
        _dropHandlers.Remove(dropHandlerExtension.DropTargetType);
      }
    }


    #endregion
  }
}